package repository

import (
	"basic-go/syntax/webook/internal/domain"
	"basic-go/syntax/webook/internal/repository/cache"
	"basic-go/syntax/webook/internal/repository/dao"
	"context"
	"database/sql"
	"time"
)

var (
	ErrUserNotFound  = dao.ErrUserNotFound
	ErrUserDuplicate = dao.ErrUserDuplicate
)

// 定义了一个接口，接口就是方法的集合
// 如果一个类型实现了接口中的所有方法，就表示它实现了该接口
type UserRepository interface {
	Create(ctx context.Context, u domain.User) error
	FindByEmail(ctx context.Context, email string) (domain.User, error)
	Update(ctx context.Context, u domain.User) error
	SelectById(ctx context.Context, id int64) (domain.User, error)
	FindOrCreateByPhone(ctx context.Context, phone string) (domain.User, error)
	FindByPhone(ctx context.Context, phone string) (domain.User, error)
	FindByWechat(ctx context.Context, openId string) (domain.User, error)
}

type CachedUserRepository struct {
	dao   dao.UserDAO
	cache cache.UserCache
}

func NewUserRepository(d dao.UserDAO, c cache.UserCache) UserRepository {
	return &CachedUserRepository{
		dao:   d,
		cache: c,
	}
}

func (r *CachedUserRepository) Create(ctx context.Context, u domain.User) error {

	return r.dao.Insert(ctx, r.domainToEntity(u))
}

func (r *CachedUserRepository) FindByWechat(ctx context.Context, openId string) (domain.User, error) {
	daoU, err := r.dao.FindByWechat(ctx, openId)
	if err != nil {
		return domain.User{}, err
	}
	return r.entityToDomain(daoU), err
}

func (r *CachedUserRepository) FindByEmail(ctx context.Context, email string) (domain.User, error) {
	daoU, err := r.dao.FindByEmail(ctx, email)
	if err != nil {
		return domain.User{}, err
	}
	return r.entityToDomain(daoU), err
}

func (r *CachedUserRepository) Update(ctx context.Context, u domain.User) error {
	err := r.dao.UpdateUser(ctx, r.domainToEntity(u))
	return err
}

func (r *CachedUserRepository) SelectById(ctx context.Context, id int64) (domain.User, error) {
	// 1、先去缓存里查
	u, err := r.cache.Get(ctx, id)
	// err为空，则直接返回，err不为空，则去数据库里查询
	if err == nil {
		return u, err
	}

	// 2、缓存里查不到、就去数据库里查找
	ud, err := r.dao.SelectById(ctx, id)
	if err != nil {
		return domain.User{}, err
	}

	u = r.entityToDomain(ud)
	// 3、从数据库查到了后 在这里设置缓存
	// 缓存设置失败，忽略这里的错误
	_ = r.cache.Set(ctx, u)
	return u, nil
}

// 缓存第二种实现思路：只有redis中key明确不存在的时候，才去数据库查询，其他时候不继续查询数据库
// 优点： redis崩溃好，不会一瞬间击溃数据库服务
// 缺点： 业务会出现不可用
func (r *CachedUserRepository) SelectByIdV1(ctx context.Context, id int64) (domain.User, error) {
	u, err := r.cache.Get(ctx, id)
	switch err {
	case nil:
		return u, err
	case cache.ErrKeyNotExist:
		ud, err := r.dao.SelectById(ctx, id)
		if err != nil {
			return domain.User{}, err
		}

		u = r.entityToDomain(ud)
		// 3、从数据库查到了后 在这里设置缓存
		// 缓存设置失败，忽略这里的错误
		_ = r.cache.Set(ctx, u)
		return u, nil
	default:
		return domain.User{}, err
	}
}

func (r *CachedUserRepository) FindOrCreateByPhone(ctx context.Context, phone string) (domain.User, error) {
	daoU, err := r.dao.SelectOrCreateByPhone(ctx, phone)
	return r.entityToDomain(daoU), err
}

func (r *CachedUserRepository) FindByPhone(ctx context.Context, phone string) (domain.User, error) {
	daoU, err := r.dao.SelectByPhone(ctx, phone)
	return r.entityToDomain(daoU), err
}

func (r *CachedUserRepository) entityToDomain(daoU dao.User) domain.User {
	return domain.User{
		Id:       daoU.Id,
		Email:    daoU.Email.String,
		Password: daoU.Password,
		Name:     daoU.Name,
		Birthday: daoU.Birthday,
		Phone:    daoU.Phone.String,
		Profile:  daoU.Profile,
		WechatInfo: domain.WechatInfo{
			UnionId: daoU.WechatUnionId.String,
			OpenId:  daoU.WechatOpenId.String,
		},
		Ctime: time.UnixMilli(daoU.Ctime),
		Utime: time.UnixMilli(daoU.Utime),
	}
}

func (r *CachedUserRepository) domainToEntity(u domain.User) dao.User {
	return dao.User{
		Id: u.Id,
		Email: sql.NullString{
			String: u.Email,
			Valid:  u.Email != "",
		},
		Password: u.Password,
		Name:     u.Name,
		Birthday: u.Birthday,
		Phone:    sql.NullString{u.Phone, u.Phone != ""},
		Profile:  u.Profile,
		WechatOpenId: sql.NullString{
			String: u.WechatInfo.OpenId,
			Valid:  u.WechatInfo.OpenId != "",
		},
		WechatUnionId: sql.NullString{
			String: u.WechatInfo.UnionId,
			Valid:  u.WechatInfo.UnionId != "",
		},
		Ctime: u.Ctime.UnixMilli(),
		Utime: u.Utime.UnixMilli(),
	}
}
